We glossed over exactly how these text strings called shaders actually get sent to OpenGL. We will go into some detail on that now.
If you are familiar with how shaders work in other APIs like Direct3D, that will not help you here. OpenGL shaders work very differently from the way they work in other APIs.
Shaders are written in a C-like language. So OpenGL uses a very C-like compilation model. In C, each individual .c file is compiled into an object file. Then, one or more object files are linked together into a single program (or static/shared library). OpenGL does something very similar.
A shader string is compiled into a shader object; this is analogous to an object file. One or more shader objects is linked into a program object.
A program object in OpenGL contains code for all of the shaders to be used for rendering. In the tutorial, we have a vertex and a fragment shader; both of these are linked together into a single program object. Building that program object is the responsibility of this code:
Example 1.6. Program Initialization
void InitializeProgram()
{
std::vector<GLuint> shaderList;
shaderList.push_back(CreateShader(GL_VERTEX_SHADER, strVertexShader));
shaderList.push_back(CreateShader(GL_FRAGMENT_SHADER, strFragmentShader));
theProgram = CreateProgram(shaderList);
std::for_each(shaderList.begin(), shaderList.end(), glDeleteShader);
}
The first statement simply creates a list of the shader objects we intend to link
together. The next two statements compile our two shader strings. The
CreateShader
function is a function defined by the tutorial
that compiles a shader.
Compiling a shader into a shader object is a lot like compiling source code. Most
important of all, it involves error checking. This is the implementation of
CreateShader
:
Example 1.7. Shader Creation
GLuint CreateShader(GLenum eShaderType, const std::string &strShaderFile) { GLuint shader = glCreateShader(eShaderType); const char *strFileData = strShaderFile.c_str(); glShaderSource(shader, 1, &strFileData, NULL); glCompileShader(shader); GLint status; glGetShaderiv(shader, GL_COMPILE_STATUS, &status); if (status == GL_FALSE) { GLint infoLogLength; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength); GLchar *strInfoLog = new GLchar[infoLogLength + 1]; glGetShaderInfoLog(shader, infoLogLength, NULL, strInfoLog); const char *strShaderType = NULL; switch(eShaderType) { case GL_VERTEX_SHADER: strShaderType = "vertex"; break; case GL_GEOMETRY_SHADER: strShaderType = "geometry"; break; case GL_FRAGMENT_SHADER: strShaderType = "fragment"; break; } fprintf(stderr, "Compile failure in %s shader:\n%s\n", strShaderType, strInfoLog); delete[] strInfoLog; } return shader; }
An OpenGL shader object is, as the name suggests, an object. So the first step is to
create the object with glCreateShader
. This function creates a
shader of a particular type (vertex or fragment), so it takes a parameter that tells
what kind of object it creates. Since each shader stage has certain syntax rules and
pre-defined variables and constants (thus making different shader stages different
dialects of GLSL), the compiler must be told what shader stage is being compiled.
Shader and program objects are objects in OpenGL. But they work rather differently from other kinds of OpenGL objects. For example, creating buffer objects, as shown above, uses a function of the form “glGen*” where * is “Buffer”. It takes a number of objects to create and a list to put those object handles in.
There are many other differences between shader/program objects and other kinds of OpenGL objects.
The next step is to actually compile the text shader into the object. The C-style
string is retrieved from the C++ std::string
object, and it is
fed into the shader object with the glShaderSource
function. The
first parameter is the shader object to put the string into. The next parameter is the
number of strings to put into the shader. Compiling multiple strings into a single
shader object works analogously to compiling header files in C files. Except of course
that the .c file explicitly lists the files it includes, while you must manually add
them with glShaderSource
.
The next parameter is an array of const char* strings. The last parameter is normally
an array of lengths of the strings. We pass in NULL
, which tells
OpenGL to assume that the string is null-terminated. In general, unless you need to use
the null character in a string, there is no need to use the last parameter.
Once the strings are in the object, they are compiled with
glCompileShader
, which does exactly what it says.
After compiling, we need to see if the compilation was successful. We do this by
calling glGetShaderiv
to retrieve the
GL_COMPILE_STATUS
. If this is GL_FALSE
, then
the shader failed to compile; otherwise compiling was successful.
If compilation fails, we do some error reporting. It prints a message to stderr that explains what failed to compile. It also prints an info log from OpenGL that describes the error; think of this log as the compiler output from a regular C compilation.
After creating both shader objects, we then pass them on to the
CreateProgram
function:
Example 1.8. Program Creation
GLuint CreateProgram(const std::vector<GLuint> &shaderList) { GLuint program = glCreateProgram(); for(size_t iLoop = 0; iLoop < shaderList.size(); iLoop++) glAttachShader(program, shaderList[iLoop]); glLinkProgram(program); GLint status; glGetProgramiv (program, GL_LINK_STATUS, &status); if (status == GL_FALSE) { GLint infoLogLength; glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength); GLchar *strInfoLog = new GLchar[infoLogLength + 1]; glGetProgramInfoLog(program, infoLogLength, NULL, strInfoLog); fprintf(stderr, "Linker failure: %s\n", strInfoLog); delete[] strInfoLog; } for(size_t iLoop = 0; iLoop < shaderList.size(); iLoop++) glDetachShader(program, shaderList[iLoop]); return program; }
This function is fairly simple. It first creates an empty program object with
glCreateProgram
. This function takes no parameters; remember
that program objects are a combination of all shader stages.
Next, it attaches each of the previously created shader objects to the programs, by
calling the function glAttachShader
in a loop over the
std::vector
of shader objects. The program does not need to
be told what stage each shader object is for; the shader object itself remembers
this.
Once all of the shader objects are attached, the code links the program with
glLinkProgram
. Similar to before, we must then fetch the
linking status by calling glGetProgramiv
with
GL_LINK_STATUS
. If it is GL_FALSE, then the linking failed and we
print the linking log. Otherwise, we return the created program.
In the above shaders, the attribute index for the vertex shader input
position
was assigned directly in the shader itself. There
are other ways to assign attribute indices to attributes besides
layout(location = #)
. OpenGL will even assign an attribute
index if you do not use any of them. Therefore, it is possible that you may not know
the attribute index of an attribute. If you need to query the attribute index, you
may call glGetAttribLocation
with the program object and a
string containing the attribute's name.
Once the program was successfully linked, the shader objects are removed from the
program with glDetachShader
. The program's linking status and
functionality is not affected by the removal of the shaders. All it does is tell OpenGL
that these objects are no longer associated with the program.
After the program has successfully linked, and the shader objects removed from the
program, the shader objects are deleted using the C++ algorithm
std::for_each.
This line loops over each of the shaders in the
list and calls glDeleteShader
on them.
Using Programs. To tell OpenGL that rendering commands should use a particular program object, the
glUseProgram
function is called. In the tutorial this is
called twice in the display
function. It is called with the
global theProgram
, which tells OpenGL that we want to use that
program for rendering until further notice. It is later called with 0, which tells
OpenGL that no programs will be used for rendering.
For the purposes of these tutorials, using program objects is not optional. OpenGL does have, in its compatibility profile, default rendering state that takes over when a program is not being used. We will not be using this, and you are encouraged to avoid its use as well.